﻿#region Copyright & licence

// This file is part of Refix.
// 
// Refix is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
// 
// Refix is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
// 
// You should have received a copy of the GNU Lesser General Public
// License along with Refix.  If not, see <http://www.gnu.org/licenses/>.
// 
// Copyright (C) 2010-11 David Musgrove and others.

#endregion

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

using Refix.Core.IO;
using Refix.Core.Options;
using Refix.Core.Properties;
using Refix.Core.Repository;
using Refix.Core.Structure;

namespace Refix.Core.Commands
{
    public class Resolve : SingleSolutionCommand
    {
        private LocalRepository _localRepository;
        private SolutionHelper _solutionHelper;
        private IEnumerable<string> _componentsToUpgrade;
        private readonly VersionNumberComparer _versionNumberComparer;

        public Resolve()
        {
            _versionNumberComparer = new VersionNumberComparer();
        }

        protected override string HelpText
        {
            get { return ResourceDispenser.GetString("RESOLVE_HELP"); }
        }

        public override bool Execute()
        {
            _localRepository = GetLocalRepository();
            _localRepository.Offline = Options.Options.Any(o => o is Offline);
            _solutionHelper = new SolutionHelper(_localRepository);

            _componentsToUpgrade = Options.Options
                .Where(o => o is Upgrade)
                .ToList()
                .ConvertAll(u => ((Upgrade)u).AssemblyName);

            var solutionFile = new SolutionFile(FileName);
            Structure.Solution solution = _solutionHelper.GetSolution(FileName);
            if (solution == null) return false;

            IEnumerable<IGrouping<string, ReferenceVersion>> components = GetComponents(solution);
            ReportAnalysis(components, ResourceDispenser.GetString("RESOLVE_INITIAL_ANALYSIS"));
            bool resolved = TryToResolveTwiceWithSyncIfNecessary(solution, components, solutionFile);

            if (resolved && _componentsToUpgrade.Count() > 0)
            {
                foreach (var componentToUpgrade in _componentsToUpgrade)
                {
                    var thisComponent = componentToUpgrade;
                    WriteUpgradeHeader(thisComponent);
                    bool upgraded = true;
                    while (upgraded)
                    {
                        upgraded = false;
                        var reference = components.First(c => c.Key == thisComponent).First();
                        string oldReferenceVersion = reference.Version;
                        var componentInRepository = _localRepository.GetComponent(thisComponent);
                        if (componentInRepository != null)
                        {
                            var versions = componentInRepository.Versions.OrderBy(v => v.Number, _versionNumberComparer);
                            var thisVersion = componentInRepository.Versions.First(v => v.Number == reference.Version);
                            foreach (var version in versions)
                            {
                                if (_versionNumberComparer.Compare(reference.Version, version.Number) >= 0)
                                {
                                    continue;
                                }
                                foreach (var componentVersion in thisVersion.AlternativeVersions)
                                {
                                    if (version.Number.IsMatchFor(componentVersion.Number))
                                    {
                                        WriteUpgradeVersionAttempt(thisComponent, version, thisVersion);
                                        TryUpgrade(solution, components, solutionFile, thisComponent, reference, version);
                                    }
                                }
                            }
                        }
                        if (reference.Version != oldReferenceVersion)
                        {
                            WriteUpgradedVersion(thisComponent, reference, oldReferenceVersion);
                            upgraded = true;
                        }
                    }
                }
            }

            if (resolved)
            {
                return true;
            }

            WriteConflictingVersionsIfVerbose(components);

            OutputWriter.WriteLine(ResourceDispenser.GetString("RESOLVE_CONFLICTING_VERSIONS"));
            if (Options.Options.Any(o => o is Graph))
            {
                new ConsoleResolutionVisualizationStructureVisitor { ResolvedComponents = components, IsPlain = Options.Options.Any(o => o is Plain) }
                    .Visit(solution);
            }
            return false;
        }

        private void TryUpgrade(Structure.Solution solution, IEnumerable<IGrouping<string, ReferenceVersion>> components, SolutionFile solutionFile, string componentToUpgrade, ReferenceVersion reference, ComponentVersion version) {
            string oldVersion = reference.Version;
            reference.Version = version.Number;
            string upgrade = componentToUpgrade;
            string versionNumber = version.Number;
            solution.Projects
                .ForEach(p => p.References
                                  .Where(r => r.Name == upgrade)
                                  .ForEach(r => r.ResolvedVersion = versionNumber));
            bool stillResolved = TryToResolve(solution, solutionFile, components);
            if (!stillResolved)
            {
                reference.Version = oldVersion;
                solution.Projects
                    .ForEach(p => p.References
                                      .Where(r => r.Name == upgrade)
                                      .ForEach(r => r.ResolvedVersion = oldVersion));
            }
        }

        static private void WriteUpgradedVersion(string componentToUpgrade, ReferenceVersion reference, string oldReferenceVersion)
        {
            using (OutputWriter.Highlight(ConsoleColor.White))
            {
                OutputWriter.WriteLine(ResourceDispenser.GetString("RESOLVE_UPGRADE_UPGRADED"),
                                       componentToUpgrade,
                                       oldReferenceVersion, reference.Version);
            }
        }

        static private void WriteUpgradeVersionAttempt(string componentToUpgrade, ComponentVersion version, ComponentVersion thisVersion)
        {
            using (OutputWriter.Highlight(ConsoleColor.White))
            {
                OutputWriter.WriteLine(OutputVerbosity.Verbose,
                                       ResourceDispenser.GetString(
                                           "RESOLVE_UPGRADE_TRYING_VERSION"),
                                       componentToUpgrade, thisVersion.Number,
                                       version.Number);
            }
        }

        static private void WriteUpgradeHeader(string componentToUpgrade)
        {
            using (OutputWriter.Highlight(ConsoleColor.White))
            {
                string message = string.Format(ResourceDispenser.GetString("RESOLVE_UPGRADE_ATTEMPTING"), componentToUpgrade);
                OutputWriter.WriteLine(OutputVerbosity.Verbose, message);
                OutputWriter.WriteLine(OutputVerbosity.Verbose, new string('=', message.Length));
            }
        }

        private bool TryToResolveTwiceWithSyncIfNecessary(Structure.Solution solution, IEnumerable<IGrouping<string, ReferenceVersion>> components, SolutionFile solutionFile)
        {
            if (TryToResolve(solution, solutionFile, components))
            {
                return true;
            }

            var sync = new Synchronize();
            sync.Options = new CommandWithOptions(sync);
            if (Options.Options.Any(o => o is Verbose))
            {
                sync.Options.Options.Add(new Verbose());
            }
            if (Options.Options.Any(o => o is Quiet))
            {
                sync.Options.Options.Add(new Quiet());
            }
            foreach (IGrouping<string, ReferenceVersion> component in
                components.Where(c => c.Select(v => v.Version).Distinct().Count() > 1
                    || c.Select(v => v.Version).First().IsNullOrEmpty()))
            {
                sync.Options.Arguments.Clear();
                sync.Options.Arguments.Add(component.Key);
                sync.Execute();
            }
            return TryToResolve(solution, solutionFile, components);
        }

        private bool TryToResolve(Structure.Solution solution, SolutionFile solutionFile, IEnumerable<IGrouping<string, ReferenceVersion>> components)
        {
            int rounds = ResolveConflictingVersions(solution, ref components);

            if (components.All(g => g.Select(v => v.Version).Distinct().Count() == 1))
            {
                WriteSuccessfullyResolvedVersions(solutionFile, solution, rounds);
                if (Options.Options.Any(o => o is Graph))
                {
                    OutputWriter.WriteLine();
                    new ConsoleResolutionVisualizationStructureVisitor
                    {ResolvedComponents = components, IsPlain = Options.Options.Any(o => o is Plain)}
                        .Visit(solution);
                }
                return true;
            }

            return false;
        }

        static private void WriteConflictingVersionsIfVerbose(IEnumerable<IGrouping<string, ReferenceVersion>> components)
        {
            if (OutputWriter.Verbository == OutputVerbosity.Verbose)
            {
                foreach (
                    IGrouping<string, ReferenceVersion> component in
                        components.Where(c => c.Select(v => v.Version).Distinct().Count() > 1))
                {
                    OutputWriter.WriteLine(ResourceDispenser.GetString("RESOLVE_COMPONENT_WITH_CONFLICT"), component.Key);
                    foreach (ReferenceVersion referenceVersion in component)
                    {
                        OutputWriter.WriteLine(ResourceDispenser.GetString("RESOLVE_VERSION"), referenceVersion.Version);
                    }
                }
            }
        }

        private void WriteSuccessfullyResolvedVersions(SolutionFile solutionFile, Structure.Solution solution, int rounds)
        {
            OutputWriter.WriteLine(ResourceDispenser.GetString("RESOLVE_NO_CONFLICTING_VERSIONS"),
                               rounds,
                               rounds == 1
                                   ? ResourceDispenser.GetString("RESOLVE_ROUND")
                                   : ResourceDispenser.GetString("RESOLVE_ROUNDS"));
            var versions = new List<BinaryReference>();
            AddModifiedVersions(versions, solution.Projects.SelectMany(p => p.References));
            foreach (BinaryReference binaryReference in versions)
            {
                OutputWriter.WriteLine(OutputVerbosity.Verbose, ResourceDispenser.GetString("RESOLVE_MAPPING_APPLIED"),
                                   binaryReference.Name, binaryReference.OriginalVersion,
                                   binaryReference.ResolvedVersion);
            }
            foreach (ProjectFile projectFile in solutionFile.CachedProjectFiles.Values)
            {
                RewriteProjectFile(versions, projectFile);
            }
        }

        private int ResolveConflictingVersions(Structure.Solution solution,
                                               ref IEnumerable<IGrouping<string, ReferenceVersion>> components)
        {
            int fixesMade = 1;
            int rounds = 0;
            while (!components.All(g => g.Select(v => v.Version).Where(n => !n.IsNullOrEmpty()).Distinct().Count() == 1) && fixesMade > 0)
            {
                rounds++;
                fixesMade = 0;
                IEnumerable<KeyValuePair<string, List<ReferenceVersion>>> conflicts = ResolveProjectVersions(components);
                Dictionary<string, string> unambiguousVersionMap = conflicts
                    .Where(v => v.Value.Where(r => r.IsCandidate).Distinct(new ReferenceVersionComparer()).Count() == 1)
                    .ToDictionary(p => p.Key, p => p.Value.Where(r => r.IsCandidate).First().Version);
                if (unambiguousVersionMap.Count > 0)
                {
                    fixesMade += solution.Projects
                        .SelectMany(p => p.References)
                        .Sum(r => FixBinaryReferenceAndRecurse(unambiguousVersionMap, r));
                }
                components = GetComponents(solution);
                ReportAnalysis(components,
                               string.Format(CultureInfo.InvariantCulture,
                                             ResourceDispenser.GetString("RESOLVE_ANALYSIS_HEADER"), rounds));
            }
            return rounds;
        }

        private void RewriteProjectFile(IEnumerable<BinaryReference> versions, ProjectFile projectFile)
        {
            ProjectFile dependencyFile = projectFile.GetDependencyFile();
            foreach (Reference reference in dependencyFile.References)
            {
                reference.ResolvedVersion = reference.SpecifiedVersion;
                Reference reference1 = reference;
                BinaryReference version = versions
                    .SingleOrDefault(v => v.Name == reference1.AssemblyName
                                          && v.OriginalVersion == reference1.SpecifiedVersion);
                if (version != null && version.ResolvedVersion != null)
                {
                    reference.ResolvedVersion = version.ResolvedVersion;
                }

                ComponentVersion componentVersion = _localRepository.GetComponentVersion(reference.AssemblyName,
                                                                                         reference.ResolvedVersion);
                if (componentVersion == null)
                {
                    continue;
                }
                AddAssemblyBindings(versions, dependencyFile, componentVersion.Dependencies);
            }
            dependencyFile.FileName = projectFile.FileName;
            dependencyFile.WriteDependencyFile();
        }

        private void AddAssemblyBindings(IEnumerable<BinaryReference> versions, ProjectFile projectFile,
                                         IEnumerable<Dependency> dependencies)
        {
            foreach (Dependency dependency in dependencies)
            {
                Dependency currentDependency = dependency;
                BinaryReference version = versions
                    .SingleOrDefault(v => v.Name == currentDependency.Name
                                          && v.OriginalVersion == currentDependency.Version);
                if (version == null)
                {
                    continue;
                }
                ComponentVersion referenceVersion = _localRepository.GetComponentVersion(dependency.Name,
                                                                                         version.OriginalVersion);
                if (!projectFile.AssemblyBindingRedirects.Any(r => r.AssemblyName == version.Name))
                {
                    var reference = new Reference
                                        {
                                            AssemblyName = version.Name,
                                            Culture = referenceVersion.Culture,
                                            PublicKeyToken = referenceVersion.PublicKey,
                                            SpecifiedVersion = version.OriginalVersion,
                                            ResolvedVersion = version.ResolvedVersion
                                        };
                    projectFile.AssemblyBindingRedirects.Add(reference);
                }
                AddAssemblyBindings(versions, projectFile, referenceVersion.Dependencies);
            }
        }

        static private void AddModifiedVersions(List<BinaryReference> versions,
                                                IEnumerable<BinaryReference> binaryReferences)
        {
            foreach (BinaryReference binaryReference in binaryReferences)
            {
                if (binaryReference.OriginalVersion != binaryReference.ResolvedVersion
                    && !versions.Contains(binaryReference, new BinaryReferenceComparer()))
                {
                    versions.Add(binaryReference);
                }
                AddModifiedVersions(versions, binaryReference.References);
            }
        }

        private int FixBinaryReferenceAndRecurse(Dictionary<string, string> versionMap,
                                                 BinaryReference binaryReference)
        {
            int fixCount = 0;
            if (versionMap.ContainsKey(binaryReference.Name) &&
                binaryReference.ResolvedVersion != versionMap[binaryReference.Name])
            {
                fixCount++;
                binaryReference.ResolvedVersion = versionMap[binaryReference.Name];
                binaryReference.References.Clear();
                _solutionHelper.AddBinaryReferences(binaryReference);
            }
            binaryReference.References.ForEach(r => fixCount += FixBinaryReferenceAndRecurse(versionMap, r));
            return fixCount;
        }

        static private IEnumerable<IGrouping<string, ReferenceVersion>> GetComponents(
            IEnumerable<BinaryReference> solution)
        {
            return solution.Distinct().GroupBy(
                r => r.Name,
                r => new ReferenceVersion
                         {
                             Version = r.ResolvedVersion
                         });
        }

        private IEnumerable<KeyValuePair<string, List<ReferenceVersion>>> ResolveProjectVersions(IEnumerable<IGrouping<string, ReferenceVersion>> components)
        {
            IEnumerable<KeyValuePair<string, List<ReferenceVersion>>> projectConflictVersions =
                components.ToDictionary(g => g.Key,
                                        g => g.Select(r => new ReferenceVersion
                                                               {
                                                                   Version = r.Version,
                                                                   IsCandidate = false
                                                               }).ToList())
                    .Where(d => d.Value.Count > 1 || (!d.Key.IsSystemAssemblyName() && d.Value[0].Version.IsNullOrEmpty())).ToList();
            var comparer = new VersionNumberComparer();
            foreach (KeyValuePair<string, List<ReferenceVersion>> projectConflictVersion in projectConflictVersions)
            {
                Component component = _localRepository.GetComponent(projectConflictVersion.Key);
                if (projectConflictVersion.Value.Count == 1 && projectConflictVersion.Value[0].Version.IsNullOrEmpty())
                {
                    projectConflictVersion.Value[0].Version =
                        component.Versions.OrderBy(v => v.Number, comparer).Last().Number;
                    projectConflictVersion.Value[0].IsCandidate = true;
                }
                foreach (ReferenceVersion candidateVersion in projectConflictVersion.Value)
                {
                    ReferenceVersion version = candidateVersion;
                    candidateVersion.IsCandidate = (from test in projectConflictVersion.Value
                                                    let testVersion =
                                                        component.Versions.Where(v => v.Number == test.Version).
                                                        SingleOrDefault()
                                                    select test.Version == version.Version
                                                           || (testVersion != null && testVersion.AlternativeVersions
                                                                                          .Any(
                                                                                              a =>
                                                                                              version.Version.
                                                                                                  IsMatchFor(a.Number))))
                        .All(matched => matched);
                }
            }
            return projectConflictVersions;
        }

        static private void ReportAnalysis(IEnumerable<IGrouping<string, ReferenceVersion>> components, string title)
        {
            if (OutputWriter.Verbository == OutputVerbosity.Verbose)
            {
                OutputWriter.WriteLine(title);
                OutputWriter.WriteLine("".PadRight(title.Length, '='));
                foreach (
                    IGrouping<string, ReferenceVersion> component in
                        components.Where(c => c.Select(v => v.Version).Distinct().Count() > 1))
                {
                    OutputWriter.WriteLine(ResourceDispenser.GetString("RESOLVE_COMPONENT_VERSION_COUNT"), component.Key,
                                       component.Select(v => v.Version).Distinct().Count());
                }
                foreach (
                    IGrouping<string, ReferenceVersion> component in
                        components.Where(c => c.Select(v => v.Version).Distinct().Count() == 1
                            && c.Select(v => v.Version).First().IsNullOrEmpty()))
                {
                    OutputWriter.WriteLine(ResourceDispenser.GetString("RESOLVE_COMPONENT_NO_VERSION"), component.Key);
                }
            }
        }
    }
}